﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using LazyMapper.Core.Helpers;

namespace LazyMapper.Core
{
    public class TypeMapping<TInput, TOutput> : ITypeMapping<TInput, TOutput>, ITypeMappingSetup<TInput, TOutput>
    {
        private readonly object _initializeLock = new object();
        private Func<TInput, TOutput> _constructor;
        private Func<TOutput> _constructorSimple;
        private bool _isInitialized;
        private readonly List<IPropertyMapping> _propertyMapping;
        private Action<TInput, TOutput> _actionAfterMap;
        private Action<TInput, TOutput> _actionBeforeMap;
        

        #region ITypeMapping<TInput,TOutput> Members

        public TypeMapping()
        {
            Name = TypeMappingHelper.GetName(typeof(TInput), typeof(TOutput));
            Key = Name.GetHashCode();
            _propertyMapping = new List<IPropertyMapping>();
        }

        public int Key { get; private set; }

        public string Name { get; private set; }

        public void Initialize(MapperInstance instance)
        {
            lock (_initializeLock)
            {
                if (_isInitialized) return;
                BuildAutoProperties(instance);
                _isInitialized = true;
            }
        }

        public bool IsMemberTypes(Type inputMemberType, Type outputMemberType)
        {
            return typeof(TInput) == inputMemberType && typeof(TOutput) == outputMemberType;
        }
        
        public object MapValues(object input, object output)
        {
            return MapValues((TInput)input, (TOutput)output);
        }

        public object Construct(object input, object output)
        {
            if (IsNull<TOutput>(output) && !IsNull<TInput>(input))
            {
                return Construct((TInput) input);
            }
            return output;
        }

        public void BuildListMapper(MapperInstance mapperInstance)
        {
            SkipAutoBuild();
            
            if (!typeof(TOutput).IsArray)
            {
                Type fromType = ReflectionHelper.DetermineEnumerableType<TInput>();
                Type toType = ReflectionHelper.DetermineEnumerableType<TOutput>();
                BuildSimpleConstructorExpresion();
                var makeGenericType = typeof(ListMapper<,>).MakeGenericType(fromType, toType);
                var instance = (IListMapper)Activator.CreateInstance(makeGenericType);
                if (mapperInstance != null) instance.SetMapper(mapperInstance);
                AfterMap((x, y) => instance.Sync(x, y));
            }
            else
            {
                throw new Exception("Automatic casting from enumerables to arrays not allowed");
            }
        }
        
        

        public TOutput Construct(TInput input)
        {
            if (_constructor != null)
            {
                return _constructor(input);
            }
            if (_constructorSimple != null)
            {
                return _constructorSimple();
            }
            return default(TOutput);
        }

        public TOutput MapValues(TInput input, TOutput output)
        {
            if (!IsNull<TInput>(input))
            {
                if (_actionBeforeMap != null) _actionBeforeMap(input, output);
                foreach (IPropertyMapping<TInput,TOutput> propertyMapping in _propertyMapping)
                {
                    propertyMapping.Map(input, output);
                }
                if (_actionAfterMap != null) _actionAfterMap(input, output);
            }
            return output;
        }

        private bool IsNull<T>(object input)
        {
            return input == (object)default(T);
        }

        public bool IsInitialized
        {
            get { return _isInitialized; }
        }

        #endregion

        #region ITypeMappingSetup<TInput,TOutput> Members

        public ITypeMappingSetup<TInput, TOutput> Constructor(Func<TInput, TOutput> input)
        {
            _constructor = input;
            return this;
        }

        public ITypeMappingSetup<TInput, TOutput> ForMember<TType>(Expression<Func<TOutput, TType>> memberExpression, Action<IMappingOption<TInput, TType>> mappingOption)
        {
            IMappingOption<TInput, TType> option = new MappingOption<TInput, TType>();
            mappingOption(option);
            _propertyMapping.Add(new PropertyMapping<TInput, TOutput, TType>(option, new ValueSetter<TOutput, TType>(memberExpression)));
            return this;
        }

        public ITypeMappingSetup<TInput, TOutput> ForMember<TType>(string name, Action<TOutput, TType> memberExpression, Action<IMappingOption<TInput, TType>> mappingOption)
        {
            IMappingOption<TInput, TType> option = new MappingOption<TInput, TType>();
            mappingOption(option);
            _propertyMapping.Add(new PropertyMapping<TInput, TOutput, TType>(option, new ValueSetter<TOutput, TType>(name,memberExpression)));
            return this;
        }

        public ITypeMappingSetup<TInput, TOutput> BeforeMap(Action<TInput, TOutput> action)
        {
            _actionBeforeMap = action;
            return this;
        }

        public ITypeMappingSetup<TInput, TOutput> AfterMap(Action<TInput, TOutput> action)
        {
            _actionAfterMap = action;
            return this;
        }

        public void SkipAutoBuild()
        {
            _isInitialized = true;
        }

        #endregion

        #region Private Methods

        private void BuildAutoProperties(MapperInstance instance)
        {
            BuildSimpleConstructorExpresion();

            var outputType = typeof(TOutput);
            var inputType = typeof(TInput);
            if (!outputType.IsClass || !inputType.IsClass) return; 

            var matches = (from outputProp in outputType.GetProperties()
                          from inputProp in inputType.GetProperties()
                          where outputProp.Name == inputProp.Name &&
                                outputProp.GetSetMethod() != null &&
                                inputProp.GetGetMethod() != null &&
                                !_propertyMapping.Select(x => x.MemberName).Contains(outputProp.Name)

                          select new PropertyMappingByProp<TInput,TOutput>(inputProp,outputProp,instance)).ToArray();

            _propertyMapping.AddRange(matches);
        }

        private void BuildSimpleConstructorExpresion()
        {
            var constructor = typeof(TOutput);
            if (_constructor != null) return;
            if (constructor.IsClass && constructor != typeof(string))
            {
                NewExpression newDictionaryExpression = Expression.New(constructor);
                var expression = Expression.Lambda<Func<TOutput>>(newDictionaryExpression);
                _constructorSimple = expression.Compile();
            }
            else if (constructor.IsInterface && (constructor.GetGenericTypeDefinition() == typeof(IList<>) || constructor.GetGenericTypeDefinition() == typeof(IEnumerable<>) ))
            {
                var makeGenericType = typeof(List<>).MakeGenericType(ReflectionHelper.DetermineEnumerableType(constructor));
                NewExpression newDictionaryExpression = Expression.New(makeGenericType);
                var expression = Expression.Lambda<Func<TOutput>>(newDictionaryExpression);
                _constructorSimple = expression.Compile();
            }
            else if (constructor.IsInterface && constructor.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                var makeGenericType = typeof(Collection<>).MakeGenericType(ReflectionHelper.DetermineEnumerableType(constructor));
                NewExpression newDictionaryExpression = Expression.New(makeGenericType);
                var expression = Expression.Lambda<Func<TOutput>>(newDictionaryExpression);
                _constructorSimple = expression.Compile();
            }
            
        }

        #endregion

        #region Equals and ToString

        public bool Equals(TypeMapping<TInput, TOutput> other)
        {
            return true;   
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof (TypeMapping<TInput, TOutput>)) return false;
            return Equals((TypeMapping<TInput, TOutput>) obj);
        }

        public override int GetHashCode()
        {
            return (_initializeLock != null ? _initializeLock.GetHashCode() : 0);
        }

        #endregion
    }
}